﻿//------------------------------------------------------------------------------
// <copyright file="Clips.cs" company="Robert MacLean">
//      Copyright © Robert MacLean.  All Rights Reserved.
// </copyright>
//
//  Details on this see: http://rule18.codeplex.com
//  This code released under the terms of the 
//  Microsoft Reciprocal License (MS-RL, http://opensource.org/licenses/ms-rl.html.)
//------------------------------------------------------------------------------
namespace Rule18.Core
{
    using System;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Windows.Forms;
    using System.Xml.Linq;
    using Rule18.Code;
    using System.Diagnostics.Contracts;

    /// <summary>
    /// Defines the in memory collection of clips and their hotkeys
    /// </summary>
    public class ClipsCollection : ObservableCollection<Clip>, IDisposable
    {
        /// <summary>
        /// The internal reference for the clips;
        /// </summary>
        private static ClipsCollection clipsInstance;

        /// <summary>
        /// Returns the instance of clips
        /// </summary>
        /// <value>The get clips.</value>
        public static ClipsCollection GetClips
        {
            get
            {
                if (clipsInstance == null)
                {
                    clipsInstance = new ClipsCollection();
                }

                return clipsInstance;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ClipsCollection"/> class.
        /// </summary>
        private ClipsCollection()
        {
            hotkeys.KeyPress += new EventHandler<FlexiEventArgs<ManagedWinapi.Hotkey>>(hotkeys_KeyPress);
        }

        /// <summary>
        /// The hotkey handler code
        /// </summary>
        private Hotkeys hotkeys = new Hotkeys();

        /// <summary>
        /// Occurs when [clip activated].
        /// </summary>
        public event EventHandler<FlexiEventArgs<string>> ClipActivated;

        /// <summary>
        /// Gets or sets the filename to save clips to.
        /// </summary>
        /// <value>The filename.</value>
        public FileInfo FileName { get; set; }


        #region Private key finding methods
        /// <summary>
        /// Finds the individual unused key.
        /// </summary>
        /// <param name="keyId">The key id.</param>
        /// <param name="ctrl">if set to <c>true</c> [CTRL].</param>
        /// <param name="alt">if set to <c>true</c> [alt].</param>
        /// <param name="shift">if set to <c>true</c> [shift].</param>
        /// <returns><c>true</c> is key is unused</returns>
        private bool FindIndividualUnusedKey(int keyId, bool ctrl, bool alt, bool shift)
        {
            return this.Where(c => c.Key != null).Count(c => c.Key.Ctrl == ctrl &&
                c.Key.Alt == alt &&
                c.Key.Shift == shift &&
                c.Key.Key == (Keys)Enum.Parse(typeof(Keys), "D" + keyId)) == 0;
        }

        /// <summary>
        /// Finds the unused key for specific modifier.
        /// </summary>
        /// <param name="modiferFlags">The modifer flags.</param>
        /// <returns></returns>
        private KeyInfo FindUnusedKeyForSpecificModifier(DefaultModifierFlags modiferFlags)
        {
            bool ctrl = modiferFlags.CtrlSet();
            bool alt = modiferFlags.AltSet();
            bool shift = modiferFlags.ShiftSet();

            for (int counter = 0; counter < 10; counter++)
            {
                if (FindIndividualUnusedKey(counter, ctrl, alt, shift))
                {
                    KeyInfo keyInfo = new KeyInfo()
                    {
                        Alt = alt,
                        Ctrl = ctrl,
                        Shift = shift,
                        Key = (Keys)Enum.Parse(typeof(Keys), "D" + counter.ToString(CultureInfo.CurrentCulture))
                    };

                    return keyInfo;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the next default key.
        /// </summary>
        /// <returns></returns>
        private KeyInfo GetNextDefaultKey()
        {
            for (int counter = 0; counter < 6; counter++)
            {
                DefaultModifierFlags modiferFlag = (DefaultModifierFlags)counter;
                KeyInfo key = FindUnusedKeyForSpecificModifier(modiferFlag);
                if (key != null)
                {
                    return key;
                }
            }

            return null;
        }
        #endregion

        /// <summary>
        /// Handles the KeyPress event of the hotkeys control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The Rule18.Code.FlexiEventArgs&lt;ManagedWinapi.Hotkey&gt; instance containing the event data.</param>
        private void hotkeys_KeyPress(object sender, FlexiEventArgs<ManagedWinapi.Hotkey> e)
        {
            ConsoleModifiers modifers = ConsoleModifiers.Alt;
            modifers.FromBoolean(e.Data.Ctrl, e.Data.Alt, e.Data.Shift);

            Clip clip = this.First(c => c.Key.Key == e.Data.KeyCode &&
                c.Key.Alt == e.Data.Alt &&
                c.Key.Ctrl == e.Data.Ctrl &&
                c.Key.Shift == e.Data.Shift);

            if (clip != null)
            {
                if (ClipActivated != null)
                {
                    FlexiEventArgs<string> eventArgs = new FlexiEventArgs<string>();
                    eventArgs.Data = clip.Text;
                    ClipActivated(this, eventArgs);
                }
            }
        }

        /// <summary>
        /// Adds the specified clip.
        /// </summary>
        /// <param name="clip">The clip.</param>
        public void AddClip(string clip)
        {
            KeyInfo shortcut = GetNextDefaultKey();
            this.Add(new Clip() { Text = clip, Key = shortcut });
            if (shortcut != null)
            {
                hotkeys.AddHotkey(shortcut.Key, shortcut.Ctrl, shortcut.Alt, shortcut.Shift);
            }

            this.Save();
        }


        /// <summary>
        /// Updates the specified clip key.
        /// </summary>
        /// <param name="id">The id.</param>
        /// <param name="shortcut">The shortcut.</param>
        /// <returns>The new ID of the updated clip.</returns>
        public Guid UpdateKey(Guid id, KeyInfo shortcut)
        {
            Contract.Requires(this.Count > 0);

            Clip clipToUpdate = this.Single(c => c.Id == id);
            return Update(id, shortcut, clipToUpdate.Text);
        }

        /// <summary>
        /// Updates the clip with the specified id.
        /// </summary>
        /// <param name="id">The id.</param>
        /// <param name="shortcut">The shortcut.</param>
        /// <param name="text">The text.</param>
        /// <returns>The new ID of the updated clip.</returns>
        private Guid Update(Guid id, KeyInfo shortcut, string text)
        {
            Contract.Requires(this.Count > 0);

            Clip clipToUpdate = this.Single(c => c.Id == id);
            Clip newClip = new Clip()
            {
                Key = shortcut,
                Text = text
            };

            int index = this.IndexOf(clipToUpdate);
            if (index < 0 || index >= this.Count)
            {
                throw new Rule18Exception("Update being called on invalid index");
            }

            Delete(id);
            hotkeys.AddHotkey(shortcut.Key, shortcut.Ctrl, shortcut.Alt, shortcut.Shift);
            this.Insert(index, newClip);
            this.Save();

            Contract.Assume(index < this.Count);
            Contract.Assume(index >= 0);
            return this[index].Id;
        }

        /// <summary>
        /// Updates the clip text.
        /// </summary>
        /// <param name="id">The id.</param>
        /// <param name="text">The text.</param>
        /// <returns>The new ID of the updated clip.</returns>
        public Guid UpdateClipText(Guid id, string text)
        {
            Contract.Requires(this.Count > 0);

            Clip clipToUpdate = this.Single(c => c.Id == id);
            return Update(id, clipToUpdate.Key, text);
        }

        /// <summary>
        /// Deletes the specified id.
        /// </summary>
        /// <param name="id">The id.</param>
        public void Delete(Guid id)
        {
            Clip clipToRemove = this.Single(c => c.Id == id);
            hotkeys.RemoveHotkey(clipToRemove.Key.Key, clipToRemove.Key.Ctrl, clipToRemove.Key.Alt, clipToRemove.Key.Shift);
            this.Remove(clipToRemove);
        }

        /// <summary>
        /// Saves the clips to a file.
        /// </summary>
        public void Save()
        {
            if (this.FileName != null)
            {                
                //todo: make option for user to disable backups
                //if (Filename.Exists)
                //{
                //    string oldFilename = Filename.FullName;
                //    string newFilename = Filename.FullName+"."+DateTime.Now.ToString("ddMMyyyyhhmmss");
                //    Filename.MoveTo(newFilename);
                //    Filename = new FileInfo(oldFilename);
                //}

                XElement rootElement = new XElement("rule18",
                    new XAttribute("ver", "1"));

                foreach (Clip clip in this)
                {
                    if (clip == null || string.IsNullOrWhiteSpace(clip.Text))
                    {
                        continue;
                    }

                    XElement clipElement = new XElement("clip",
                        new XAttribute("text", clip.Text));

                    if (clip.Key != null)
                    {
                        clipElement.Add(new XElement("shortcut",
                            new XAttribute("blank", false),
                            new XAttribute("key", clip.Key.Key),
                            new XAttribute("ctrl", clip.Key.Ctrl),
                            new XAttribute("alt", clip.Key.Alt),
                            new XAttribute("shift", clip.Key.Shift)));
                    }
                    else
                    {
                        clipElement.Add(new XElement("shortcut",
                            new XAttribute("blank", true)));
                    }

                    rootElement.Add(clipElement);
                }

                Contract.Assume(String.IsNullOrEmpty(this.FileName.FullName) == false);
                new XDocument(rootElement).Save(this.FileName.FullName);
            }
        }

        /// <summary>
        /// Loads the clips from a specified filename.
        /// </summary>
        /// <param name="fileName">The filename.</param>
        public void Load(string fileName)
        {
            Contract.Requires(string.IsNullOrEmpty(fileName) == false);

            this.Save();
            this.ClearClips();

            FileName = new FileInfo(fileName);

            XDocument document = XDocument.Load(fileName);
            foreach (XElement element in document.Descendants("clip"))
            {
                XElement shortcutElement = element.Descendants("shortcut").Single();
                KeyInfo key = null;

                if (Convert.ToBoolean(shortcutElement.Attribute("blank").Value, CultureInfo.CurrentCulture) == false)
                {
                    key = new KeyInfo()
                    {
                        Key = (Keys)Enum.Parse(typeof(Keys), shortcutElement.Attribute("key").Value),
                        Alt = Convert.ToBoolean(shortcutElement.Attribute("alt").Value, CultureInfo.CurrentCulture),
                        Ctrl = Convert.ToBoolean(shortcutElement.Attribute("ctrl").Value, CultureInfo.CurrentCulture),
                        Shift = Convert.ToBoolean(shortcutElement.Attribute("shift").Value, CultureInfo.CurrentCulture)
                    };
                }

                this.Add(new Clip() { Text = element.Attribute("text").Value, Key = key });

                if (key != null)
                {
                    hotkeys.AddHotkey(key.Key, key.Ctrl, key.Alt, key.Shift);
                }
            }
        }

        /// <summary>
        /// Clears this instance.
        /// </summary>
        public void ClearClips()
        {
            this.Save();

            var ids = (from v in this
                       select v.Id).ToArray();

            foreach (Guid id in ids)
            {
                this.Delete(id);
            }

            if (this.FileName != null)
            {
                this.FileName = null;
            }
        }


        #region IDisposable Members

        /// <summary>
        /// Internal variable which checks if Dispose has already been called
        /// </summary>
        private Boolean disposed;

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        private void Dispose(Boolean disposing)
        {
            if (disposed)
            {
                return;
            }

            if (disposing)
            {
                hotkeys.Dispose();
            }

            disposed = true;
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            // Call the private Dispose(bool) helper and indicate 
            // that we are explicitly disposing
            this.Dispose(true);

            // Tell the garbage collector that the object doesn't require any
            // cleanup when collected since Dispose was called explicitly.
            GC.SuppressFinalize(this);
        }

        #endregion
    }
}
